package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.SystemConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

	private final StringRedisTemplate stringRedisTemplate;

	private final CacheClient cacheClient;

	@Override
	public Result queryById(Long id) {
		//缓存穿透,直接调用工具类方法即可实现缓存穿透问题的解决
		Shop shop1 = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
		//Shop shop = queryWithPassThrough(id);

		//互斥锁解决缓存击穿问题
		//Shop shop = queryWithMutex(id);

		//逻辑过期解决缓存击穿问题
		Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
		//Shop shop = queryWithLogicalExpire(id);

		//给前端友好提示
		if (ObjectUtils.isNull(shop)) {
			return Result.fail("店铺不存在");
		}

		//7.返回
		return Result.ok(shop);
	}

	/*//线程池，在缓存击穿时持锁线程开启新线程进行缓存重构操作
	private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

	//利用逻辑过期解决缓存击穿问题
	public Shop queryWithLogicalExpire(Long id) {
		String key = CACHE_SHOP_KEY + id;
		//1.从Redis中查询商品缓存
		String shopJson = stringRedisTemplate.opsForValue().get(key);
		//2.判断是否存在
		if (StrUtil.isBlank(shopJson)) {
			//存在，直接返回
			return null;
		}
		//4.命中，需要先把JSON反序列化为对象
		RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
		Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
		LocalDateTime expireTime = redisData.getExpireTime();
		//5.判断是否过期
		if (expireTime.isAfter(LocalDateTime.now())) {
			//5.1未过期，直接返回店铺信息
			return shop;
		}
		//5.2已过期，需要缓存重建
		//6.重建缓存
		//6.1获取互斥锁
		String lockKey = LOCK_SHOP_KEY + id;
		boolean isLock = tryLock(lockKey);
		//6.2判断是否获取成功
		if (isLock) {
			// 6.3获取锁成功，实现缓存重建
			CACHE_REBUILD_EXECUTOR.submit(() -> {
				try {
					//重建缓存
					this.saveShop2Redis(id, 20L);
				} catch (Exception e) {
					throw new RuntimeException(e);
				} finally {
					//释放锁
					unLock(lockKey);
				}
			});
		}
		//6.4返回过期的商铺信息
		return shop;
	}

	//基于互斥锁的方式解决缓存击穿问题
	public Shop queryWithMutex(Long id) {
		//构造key的生成规则
		String key = CACHE_SHOP_KEY + id;
		//1.从Redis中查询商品缓存
		String shopJson = stringRedisTemplate.opsForValue().get(key);
		//2.判断是否存在
		if (StrUtil.isNotBlank(shopJson)) {
			//3存在，直接返回
			return BeanUtil.toBean(shopJson, Shop.class);
		}
		//判断命中的是否是空值（解决缓存击穿）
		if (shopJson != null) {
			return null;
		}

		//4.实现缓存重建
		//4.1获取互斥锁
		String lockKey = "lock:shop:" + id;
		Shop shop = null;
		try {
			boolean isLock = tryLock(lockKey);
			//4.2判断是否获取成功
			if (!isLock) {
				//4.3失败，则休眠后重试
				Thread.sleep(50);
				//递归调用，实现重试
				return queryWithMutex(id);
			}

			//4.4不存在，根据id查询数据库
			shop = getById(id);
			//为了测试缓存击穿的效果，这里模拟重建的延时，以此来增加延迟，从而更明显地看到缓存重建的效果
			Thread.sleep(200);
			//5.数据库不存在，返回错误
			if (shop == null) {
				//将空值写入Redis（解决缓存击穿），缓存空对象
				stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
				//返回错误信息
				return null;
			}
			//6.存在，写入Redis，添加TTL
			stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		} finally {
			//7.释放互斥锁,无论是否有异常都需要将锁释放，否则就成为死锁
			unLock(lockKey);
		}

		//8.返回
		return shop;
	}*/

	//缓存穿透方法封装
	/*public Shop queryWithPassThrough(Long id) {
		String key = CACHE_SHOP_KEY + id;
		//1.从Redis中查询商品缓存
		String shopJson = stringRedisTemplate.opsForValue().get(key);
		//2.判断是否存在
		if (StrUtil.isNotBlank(shopJson)) {
			//存在，直接返回
			return BeanUtil.toBean(shopJson, Shop.class);
		}
		//判断命中的是否是空值（解决缓存穿透）
		if (shopJson != null) {
			return null;
		}
		//4.不存在，根据id查询数据库
		Shop shop = getById(id);
		//5.数据库不存在，返回错误
		if (shop == null) {
			//将空值写入Redis（解决缓存穿透），缓存空对象
			stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
			//返回错误信息
			return null;
		}
		//6.存在，写入Redis，添加TTL
		stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
		//7.返回
		return shop;
	}*/

	//尝试获取锁
	/*private boolean tryLock(String key) {
		Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
		//不直接返回flag的原因是防止flag为null时，报空指针异常，所以用Boolean.TRUE.equals(flag)[防御性编程]
		return Boolean.TRUE.equals(flag);
	}

	//尝试释放锁
	private void unLock(String key) {
		stringRedisTemplate.delete(key);
	}*/

	//缓存预热,将热点key提前缓存到Redis里去
	/*public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
		//1.查询店铺数据
		Shop shop = getById(id);
		Thread.sleep(200);
		//2.封装逻辑过期时间
		RedisData redisData = new RedisData();
		redisData.setData(shop);
		//设置逻辑过期时间
		redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
		//3.写入Redis
		stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
	}*/

	@Override
	@Transactional
	public Result update(Shop shop) {
		Long id = shop.getId();
		if (id == null) {
			return Result.fail("店铺ID不能为空");
		}
		//1.更新数据库
		updateById(shop);
		//2.删除Redis缓存
		stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
		return Result.ok();
	}

	@Override
	public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
		// 1.判断是否需要根据坐标查询
		if (x == null || y == null) {
			// 不需要坐标查询，按数据库查询
			Page<Shop> page = query()
					.eq("type_id", typeId)
					.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
			// 返回数据
			return Result.ok(page.getRecords());
		}

		// 2.计算分页参数
		int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
		int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

		// 3.查询redis、按照距离排序、分页。结果：shopId、distance
		String key = SHOP_GEO_KEY + typeId;
		GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
				.search(
						key,// key，此处是"shop:geo:"+店铺类型id
						GeoReference.fromCoordinate(x, y),//经纬度，店铺经纬度
						new Distance(5000),//半径，单位默认是米
						RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)//控制地理位置搜索的返回结果格式和数量限制
				);
		// 4.解析出id
		if (results == null) {
			return Result.ok(Collections.emptyList());
		}
		List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
		if (list.size() <= from) {
			// 没有下一页，直接结束，避免SQL执行空的in操作
			return Result.ok(Collections.emptyList());
		}
		// 4.1.截取 from ~ end的部分
		List<Long> ids = new ArrayList<>(list.size());
		Map<String, Distance> distanceMap = new HashMap<>(list.size());
		list.stream().skip(from).forEach(result -> {
			// 4.2.获取店铺id
			String shopIdStr = result.getContent().getName();
			ids.add(Long.valueOf(shopIdStr));
			// 4.3.获取距离
			Distance distance = result.getDistance();
			distanceMap.put(shopIdStr, distance);
		});
		// 5.根据id查询Shop
		String idStr = StrUtil.join(",", ids);
		List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
		for (Shop shop : shops) {
			shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
		}
		// 6.返回
		return Result.ok(shops);
	}
}
